package diagnostic

import (
	"encoding/json"
	"fmt"
	"mi/compiler/ast"
	"mi/internal/xpath"
	"strconv"
	"strings"
)

type Kind int
const (
	Warning Kind = iota
	Error
	Information
)

func (k Kind) ToString() string {
	switch k {
	case Warning:
		return "警告"
	case Error:
		return "错误"
	case Information:
		return "信息"
	default:
		return ""
	}
}

type Message struct {
	Code     int
	Category Kind
	Message  string
}

// A linked list of formatted diagnostic messages to be used as part of a multiline message.
// It is built from the bottom up, leaving the head to be the "main" 
// While it seems that MessageChain is structurally similar to Message,
// the difference is that messages are all preformatted in DMC.
type MessageChain struct {
	MessageText string
	Category    Kind
	Code        int
	Next        *MessageChain
}

type Diagnostic struct {
	File         *ast.SourceFile
	Start        int
	Length       int
	MessageText  string
	MessageChain *MessageChain
	Category     Kind
	Code         int
}

func toString(value interface{}) string {
	var key string
	if value == nil {
		return key
	}

	switch f := value.(type) {
	case float64:
		key = strconv.FormatFloat(f, 'f', -1, 64)
	case float32:
		key = strconv.FormatFloat(float64(f), 'f', -1, 64)
	case int:
		key = strconv.Itoa(f)
	case uint:
		key = strconv.Itoa(int(f))
	case int8:
		key = strconv.Itoa(int(f))
	case uint8:
		key = strconv.Itoa(int(f))
	case int16:
		key = strconv.Itoa(int(f))
	case uint16:
		key = strconv.Itoa(int(f))
	case int32:
		key = strconv.Itoa(int(f))
	case uint32:
		key = strconv.Itoa(int(f))
	case int64:
		key = strconv.FormatInt(f, 10)
	case uint64:
		key = strconv.FormatUint(f, 10)
	case string:
		key = value.(string)
	case []byte:
		key = string(value.([]byte))
	default:
		newValue, _ := json.Marshal(value)
		key = string(newValue)
	}

	return key
}

func formatStringFromArgs(text string, args ...interface{}) string {
	for i, arg := range args {
		text = strings.ReplaceAll(text, fmt.Sprintf("{%d}", i), toString(arg))
	}
	return text
}

func CreateFileDiagnostic(file *ast.SourceFile, start int, length int, msg *Message, args ...interface{}) *Diagnostic {
	if start < 0 {
		panic( "start must be non-negative, is "+ strconv.Itoa(start))
	}
	if length < 0 {
		panic("length must be non-negative, is "+ strconv.Itoa(length))
	}

	var text = msg.Message
	if len(args) > 0 {
		text = formatStringFromArgs(text, args ...)
	}

	return &Diagnostic{
		File:        file,
		Start:       start,
		Length:      length,
		MessageText: text,
		Category:    msg.Category,
		Code:        msg.Code,
	}
}

func CreateCompilerDiagnostic(message *Message, args ... interface{}) *Diagnostic {
	var text = message.Message
	if len(args) > 0 {
		text = formatStringFromArgs(text, args ...)
	}

	return &Diagnostic{
		MessageText: text,
		Category: message.Category,
		Code: message.Code,
	}
}

func ChainDiagnosticMessages(details *MessageChain, message *Message, args ... interface{}) *MessageChain {
	var text = message.Message
	text = formatStringFromArgs(text, args ...)

	return &MessageChain{
		MessageText: text,
		Category:    message.Category,
		Code:        message.Code,
		Next:        details,
	}
}

func FlattenDiagnosticChain(file *ast.SourceFile, start int, length int, diagnosticChain *MessageChain, newLine string) *Diagnostic {
	if start < 0 {
		panic( "start must be non-negative, is "+strconv.Itoa(start))
	}
	if length < 0 {
		panic("length must be non-negative, is "+strconv.Itoa(length))
	}

	var code = diagnosticChain.Code
	var category = diagnosticChain.Category
	var messageText = ""

	var indent = 0
	for diagnosticChain != nil {
		if indent > 0 {
			messageText += newLine
			for i := 0; i < indent; i++ {
				messageText += "  "
			}
		}
		messageText += diagnosticChain.MessageText
		indent++
		diagnosticChain = diagnosticChain.Next
	}

	return &Diagnostic{
		File:        file,
		Start:       start,
		Length:      length,
		MessageText: messageText,
		Category:    category,
		Code:        code,
	}
}

func getDiagnosticFilename(diagnostic *Diagnostic) xpath.URI {
	if diagnostic.File != nil {
		var sourceFile = diagnostic.File
		return sourceFile.Uri
	} else {
		return ""
	}
}

func compareIntValue(a, b int) int {
	if a == b { return 0 }
	if a < b {
		return -1
	} else {
		return 1
	}
}

func compareStringValue(a, b string) int {
	if a == b { return 0 }
	if a < b {
		return -1
	} else {
		return 1
	}
}

func compareDiagnostics(d1 *Diagnostic, d2 *Diagnostic) int {
	//if val := xpath.CompareURI(getDiagnosticFilename(d1), getDiagnosticFilename(d2)); val != 0 { return val }
	if val := compareIntValue(d1.Start, d2.Start); val != 0 { return val }
	if val := compareIntValue(d1.Length, d2.Length); val != 0 { return val }
	if val := compareIntValue(d1.Code, d2.Code); val != 0 { return val }
	if val := compareStringValue(d1.MessageText, d2.MessageText); val != 0 { return val }
	return 0
}

func SortDiagnostic(arr []*Diagnostic) {
	for i := 0 ; i < len(arr) ; i++ {
		for j := i + 1; j < len(arr); j++ {
			if compareDiagnostics(arr[i], arr[j]) > 0 {
				arr[i], arr[j] = arr[j], arr[i]
			}
		}
	}
}

func SortAndDeduplicateDiagnostics(diagnostics []*Diagnostic) []*Diagnostic {
	SortDiagnostic(diagnostics)
	return DeduplicateSortedDiagnostics(diagnostics)
}

func DeduplicateSortedDiagnostics(diagnostics []*Diagnostic) []*Diagnostic {
	if len(diagnostics) < 2 {
		return diagnostics
	}

	var newDiagnostics = []*Diagnostic{diagnostics[0]}
	var previousDiagnostic = diagnostics[0]
	for i := 1; i < len(diagnostics); i++ {
		var currentDiagnostic = diagnostics[i]
		var isDupe = compareDiagnostics(currentDiagnostic, previousDiagnostic) == 0
		if !isDupe {
			newDiagnostics = append(newDiagnostics, currentDiagnostic)
			previousDiagnostic = currentDiagnostic
		}
	}

	return newDiagnostics
}

func FilterDiagnostic(arr []*Diagnostic, f func(x *Diagnostic) bool ) []*Diagnostic {
	var result []*Diagnostic
	for _, item := range arr {
		if f(item) {
			result = append(result, item)
		}
	}
	return result
}